# ----------------------------------------------------------------------------
# pytform
# Copyright (c) 2008, Michael Henrique Siegwarth
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions 
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright 
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pytform nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

import yaml

import Box2D2 as box2d
import pyglet.graphics
from pyglet.gl import *

from event import *
from collision_listener import *
from objects import *

class Level(object):
    def __init__(self, level_name, app, textures=None):
        # App instance
        self.app = app
        
        # Textures used in the loaded map
        self.used_textures = dict()
        
        # List of loaded strips
        self.strips = []

        # Terrain batch (pyglet rendering optimization)
        self.terrain_batch = pyglet.graphics.Batch()

        # Object batch
        self.object_batch = pyglet.graphics.Batch()

        # Background batch
        self.background_batch = pyglet.graphics.Batch()

        # Loaded map file name
        self.map_file_name = "levels/" + level_name + "/" + level_name + ".yaml"

        # Map file being read
        m = open(self.map_file_name, "r")
        map_file_content = yaml.load(m.read())
        m.close()

        worldAABB = box2d.b2AABB()
        worldAABB.lowerBound.Set(-10, -10)
        worldAABB.upperBound.Set(500, 200)

        self.gravity = map_file_content["world"]["gravity"]
        gravity = box2d.b2Vec2(self.gravity[0], self.gravity[1])
         
        self.world = box2d.b2World(worldAABB, gravity, True)

        self.contact_listener = CollisionListener(self)
        self.world.SetContactListener(self.contact_listener)
    
        body_def = box2d.b2BodyDef()
        body_def.position.Set(0, 0)
        
        self.body = self.world.CreateBody(body_def)
        self.body.SetMassFromShapes()

        self.map_offset = map_file_content["screen"]["offset"]
        self.ppm = map_file_content["screen"]["ppm"]

        # Player object's start position
        self.player_start_position = map_file_content['player']['start_position']
        self.player_class = map_file_content['player']['class']

        self.load_textures(map_file_content["textures"], textures)
        self.load_terrain(map_file_content["strips"])
        self.load_objects(map_file_content["objects"], textures)
        self.load_background(map_file_content["background"])
        self.load_events(map_file_content["events"])

    def add_event(self, event):
        if isinstance(event, LevelEvent):
            if event.event_type == "a":
                self.level_add_events.append(event)
            elif event.event_type == "r":
                self.level_rem_events.append(event)        
        elif isinstance(event, ObjectEvent):
            if event.event_type == "a":
                self.object_add_events.append(event)
            elif event.event_type == "r":
                self.object_rem_events.append(event) 

    def remove_event(self, event):
        if isinstance(event, LevelEvent):
            if event.event_type == "a":
                self.level_add_events.remove(event)
            elif event.event_type == "r":
                self.level_rem_events.remove(event)        
        elif isinstance(event, ObjectEvent):
            if event.event_type == "a":
                self.object_add_events.remove(event)
            elif event.event_type == "r":
                self.object_rem_events.remove(event)

    def get_events(self):
        return self.level_add_events + self.level_rem_events

    def set_events(self):
        print "Can't set level_events"

    level_events = property(get_events, set_events)

    def load_events(self, events):
        # Events
        self.level_add_events = []
        self.level_rem_events = []
        self.object_add_events = []
        self.object_rem_events = []
        self.time_events = []

        # Load events from map
        for e in events:
            if len(e) == 4:
                new_event = LevelEvent(self.get_object_by_id(e[1]),
                                        self.get_object_by_id(e[2]),
                                        e[3],
                                        e[0],
                                        eval("self." + e[3]))
                self.add_event(new_event)
            elif len(e) == 2:
                # Load timed events
                self.time_events.append(TimeEvent(e[0], e[1], eval("self." + e[1])))

        # Load default events for objects
        temp_events = []
        for o in self.objects:
            for e in o.events:
                if e[1] != "strip.TerrainTriangleStrip":
                    for o2 in self.objects:
                        if o != o2:                    
                            name = str(o2.__class__)
                            name = name.replace("<class 'objects.", "")
                            name = name.replace("'>", "")
                            if name == e[1]:     
                                new_event = ObjectEvent(o, o2, e[0], e[2])
                                self.add_event(new_event)
                else:
                    # If it is a terrain strip, add the level
                    # itself. It has its body, which contains all
                    # the terrain.
                    new_event = ObjectEvent(o, self, e[0], e[2])
                    self.add_event(new_event)


    def load_background(self, background):
        # Background
        self.background = []
        self.background_distance = background['distance']
        self.background_size = background['size']
        self.background_offset = background['offset']
        bg = background['bg']

        for row in bg:
            l = []
            for item in row:
                l.append(self.used_textures[item])
            self.background.append(l)

        self.background.reverse()

        w = self.background_size[0]
        h = self.background_size[1]

        count1 = 0
        count2 = 0

        for row in self.background:
            for quad_tex in row:
                group = pyglet.graphics.TextureGroup(quad_tex.get_texture())
                self.background_batch.add(4, pyglet.gl.GL_QUADS, group,
                    ('v2f/static', (count2, count1,
                                    count2, count1 + h,
                                    count2 + w, count1 + h,
                                    count2 + w, count1)),
                    ('t2f/static', (0, 0, 0, 1, 1, 1, 1, 0)),
                    ('c3f/static', (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
                )

                count2 += w
            count1 += h
            count2 = 0

    def load_objects(self, objs, is_editor):
        # All objects, like player, monsters... objects!
        self.objects = []

        # Id to be used when creating new objects in the editor
        # See next_id property
        self._next_id = 1

        # Create player object and put it in the objects list
        self.player_object = eval(self.player_class)(
            self.player_start_position[0], self.player_start_position[1],
            self.player_start_position[2], self.world, self.object_batch)
        self.objects.append(self.player_object)

        for o in objs:
            if o[1] != "quad.EventArea":
                    x = eval(o[1])(o[2], o[3], o[4], self.world,
                                   self.object_batch)
            else:
                if is_editor != None:
                    x = eval(o[1])(o[2], o[3], o[4], o[5], o[6],
                                   self.world, self.object_batch)
                else:
                    # In the game, don't draw event areas
                    x = eval(o[1])(o[2], o[3], o[4], o[5], o[6],
                                   self.world, None)

            x.id = o[0]
            self.objects.append(x)

            # Look for the largest id
            if self._next_id < o[0]:
                self._next_id = o[0]

    def load_textures(self, textures_from_file, textures_from_editor):
        if textures_from_editor == None:
            for t in textures_from_file:
                # t[0] --> texture id in file
                # t[1] --> name
                tex = pyglet.image.load("textures/" + t[1])
                setattr(tex, "file", t[1])
                self.used_textures[t[0]] = tex
        else:
            for t in textures_from_file:
                for t2 in textures_from_editor:
                    if t[1] == t2.file:                
                        self.used_textures[t[0]] = t2
                        break

    def load_terrain(self, strips):
        for s in strips:
            if s[3] == 0:
                texture = None
            else:
                texture = self.used_textures[s[3]]

            st = strip.TerrainTriangleStrip(s[0], s[1], s[2], texture,
                                           self.body,
                                           self.terrain_batch)
            self.strips.append(st)

    def save(self):
        self.update_used_textures()
        
        dic = dict()

        dic["world"] = dict()
        dic["world"]["gravity"] = str(self.gravity)

        dic["screen"] = dict()        
        dic["screen"]["offset"] = str(self.map_offset)
        dic["screen"]["ppm"] = self.ppm

        dic["player"] = dict()
        o = self.get_object_by_id(1)
        l = list(o.position)
        l.append(o.angle)
        dic["player"]["start_position"] = str(l)
        dic["player"]["class"] = self.player_class

        dic["textures"] = []
        for index, tex in self.used_textures.iteritems():
            dic["textures"].append(str([index, tex.file]))
        
        dic["strips"] = []
        for s in self.strips:
            strip = s.like_array()
            if s.texture != None:
                strip.append(self.get_texture(s.texture.file))
            dic["strips"].append(str(strip))

        dic["objects"] = []
        for o in self.objects:
            l = []
            # TODO - Make a more friendly code
            name = str(o.__class__)
            name = name.replace("<class 'objects.", "")
            name = name.replace("'>", "")

            if name != self.player_class:
                l.append(o.id)
                l.append(name)
                l.append(o.position[0])
                l.append(o.position[1])
                l.append(o.angle)

                if name == "quad.EventArea":  
                    l.append(o.hw * 2)
                    l.append(o.hh * 2)

                dic["objects"].append(str(l))

        dic["background"] = dict()
        dic["background"]["bg"] = []
        self.background.reverse()
        for row in self.background:
            l = []
            for item in row:
                l.append(self.get_texture(item.file))
            dic["background"]["bg"].append(str(l))
        self.background.reverse()
        dic["background"]["distance"] = self.background_distance
        dic["background"]["size"] = str(self.background_size)
        dic["background"]["offset"] = str(self.background_offset)

        dic["events"] = []

        for e in self.level_events:
            l = []
            l.append(e.event_type)
            l.append(e.source_object.id)
            l.append(e.trigger_object.id)
            l.append(e.name)
            dic["events"].append(str(l))

        for e in self.time_events:
            l = []
            l.append(e.interval)
            l.append(e.name)
            dic["events"].append(str(l))
            
        dumped = yaml.dump(dic, default_flow_style=False).replace("\'", "")

        m = open(self.map_file_name, "w")
        m.write(dumped)
        m.close()

    def activate_time_events(self):
        for e in self.time_events:
            pyglet.clock.schedule_interval(e.func, e.interval)

    def deactivate_time_events(self):
        # TODO - This method should be in the classes destructor.
        # But, due to a reference counting bug, it won't be called.
        for e in self.time_events:
            pyglet.clock.unschedule(e.func)

    def get_events_from_object(self, obj):
        l = []
        for e in self.level_events:
            if e.source_object == obj or e.trigger_object == obj:
                l.append(e)

        if len(l) == 0:
            return None
        else:
            return l

    def get_object_by_id(self, obj_id):
        for o in self.objects:
            if o.id == obj_id:
                return o

        return None

    def get_next_id(self):
        self._next_id += 1
        return self._next_id

    def set_next_id(self, next_id):
        pass

    next_id = property(get_next_id, set_next_id)

    def get_texture(self, file_name):
        for index, tex in self.used_textures.iteritems():
            if file_name == tex.file:
                return index

        return 0

    def update_used_textures(self):
        """Updates the used_textures variable with all textures that
        are currently being used by the triangles loaded"""
        counter = 1

        textures_dict = dict()
        
        for s in self.strips:
            if s.texture != None:
                # If the strip has a texture, see if it is in the
                # textures dictionary
                for index, tex in textures_dict.iteritems():
                    if tex.file == s.texture.file:
                        break
                else:
                    textures_dict[counter] = s.texture
                    counter += 1
                        
        self.used_textures = textures_dict

    def draw(self, map_offset):
        # Draw background
        glPushMatrix()
        glTranslatef(self.background_offset[0] + map_offset[0]/self.background_distance,
                     self.background_offset[1] + map_offset[1]/self.background_distance, 0)
        self.background_batch.draw()
        glPopMatrix()

        # Draw all strips
        self.terrain_batch.draw()
        
        # Draw all objects
        self.object_batch.draw()

    def create_triangle(self, coords, tv, color, texture):
        """Create new triangle, based on new_triangle_coords"""
        new_triangle = triangle.TerrainTriangle(coords,
                            [(tv[0], tv[1]),
                             (tv[2], tv[3]),
                             (tv[4], tv[5])],
                            [(color[0], color[1], color[2]),
                             (color[3], color[4], color[5]),
                             (color[6], color[7], color[8])],
                            texture, self.body, self.terrain_batch)
        self.triangles.append(new_triangle)
    
        return new_triangle

    def step(self):
        remove_add_list = []
        remove_rem_list = []

        self.world.Step(1.0 / 60, 10, 8)

        for o in self.objects:
            try:
                o.step()
            except AttributeError:
                pass

        for e in self.level_add_events:
            e.run()

            if e.run_once == True:
                remove_add_list.append(e)

        for e in remove_add_list:
            self.level_add_events.remove(e)

        for e in self.level_rem_events:
            e.run()

            if e.run_once == True:
                remove_rem_list.append(e)

        for e in remove_rem_list:
            self.level_rem_events.remove(e)

        for e in self.object_add_events:
            e.run()

        for e in self.object_rem_events:
            e.run()

        for o in self.objects:
            if not o.body.IsSleeping():
                o.update_vertices()
        
    def get_strip_in(self, position):
        s = None
        for strip in self.strips:
            if strip.contains(position):
                s = strip
        else:
            return s

    def get_object_in(self, position):
        for obj in self.objects:
            if obj.contains(position):
                return obj
        else:
            return None

    def remove_strip(self, strip):
        try:
            # TODO - This should go to the object's destructor, but
            # there a crazy reference counting bug
            strip.delete_vertex_lists()
            self.strips.remove(strip)
            del strip
        except ValueError:
            return None

        if len(self.strips) != 0:
            return self.strips[-1]
        else:
            return None

    def remove_object(self, obj):
        try:
            # TODO - This should go to the object's destructor, but
            # there a crazy reference counting bug
            obj.delete_vertex_lists()
            self.objects.remove(obj)
            del obj
        except ValueError:
            return None

        if len(self.objects) != 0:
            return self.objects[-1]
        else:
            return None
        
    def get_event_names(self):
        methods = dir(self)
        new_list = []
        for m in methods:
            if m.find("event_") != -1:
                new_list.append(m)

        new_list.remove("get_event_names")
        return new_list
